X11 剪切板

参考:

首先在 X11 上剪切板被称作 Selections,系统上可以有任意多个 Selections,并且有三个预定义的标准 Selections:

  • primary - 当前选中的文本,例如终端上点击鼠标中键可以立即粘贴选中的文本
  • secondary - 没有被使用
  • clipboard - 通常意义上的剪切板,不同进程间交换数据时使用它

只有 Selection 的拥有者(owner)可以修改它,其它进程只能读取。owner 需要负责保存 Selection 的内容,并在其它进程请求读取的时候返回给对方。

当用户使用 Ctrl + C 进行复制时,当前应用就会申请成为 clipboard 的拥有者,之前的拥有者需要让出。

如果应用程序只希望在内部复制粘贴数据,不希望与其它进程交换信息,可以创建一个任意命名的属于自己的剪切板。

以下代码可以查看 clipboard 的当前 owner:

1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <xcb/xcb.h>
5
6int main(void)
7{
8    // 建立连接
9    xcb_connection_t* conn = xcb_connect(NULL, NULL);
10
11    // 获取 screen
12    const xcb_setup_t* setup = xcb_get_setup(conn);
13    xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
14    xcb_screen_t* screen = iter.data;
15
16    // 获取 root 窗口
17    xcb_window_t root = screen->root;
18
19    // 创建 ATOM
20    xcb_atom_t nameAtom;
21    {
22        const char* name = "PRIMARY";
23        xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, 0, strlen(name), name);
24        xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(conn, cookie, NULL);
25        nameAtom = reply->atom;
26        free(reply);
27    }
28    
29    // 读取剪切板的 owner
30    xcb_window_t owner;
31    {
32        xcb_get_selection_owner_cookie_t cookie =  xcb_get_selection_owner(conn, nameAtom);
33        xcb_get_selection_owner_reply_t* reply = xcb_get_selection_owner_reply(conn, cookie, NULL);
34        owner = reply->owner;
35        free(reply);
36    }
37
38    // 读取窗口名称
39    #define BUFFER_SIZE 64
40    char name[BUFFER_SIZE] = {0};
41    {
42        xcb_get_property_cookie_t cookie = xcb_get_property(conn, 0, owner, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, BUFFER_SIZE/4);
43        xcb_get_property_reply_t* reply = xcb_get_property_reply(conn, cookie, NULL);
44        int len = xcb_get_property_value_length(reply);
45        len = len > 64 ? 64 : len;
46        strncpy(name, xcb_get_property_value(reply), len);
47        free(reply);
48    }
49    #undef BUFFER_SIZE
50
51    // 打印
52    printf("Owner ID: 0x%x\n", owner);
53    printf("Owner Name: %s\n", name);
54}

以下代码可以将自己设为 clipboard 的 owner,并将所有的粘贴内容替换为当前的时间字符串:

1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <time.h>
5#include <xcb/xcb.h>
6#include <xcb/xfixes.h>
7
8int main(void)
9{
10    // 建立连接
11    xcb_connection_t* conn = xcb_connect(NULL, NULL);
12
13    // 获取 screen
14    const xcb_setup_t* setup = xcb_get_setup(conn);
15    xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
16    xcb_screen_t* screen = iter.data;
17
18    // 获取 root 窗口
19    xcb_window_t root = screen->root;
20
21    // 创建一个窗口
22    xcb_window_t window = xcb_generate_id(conn);
23    {
24        uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
25        uint32_t values[2];
26        values[0] = screen->white_pixel;
27        values[1] = XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE;
28        xcb_create_window(conn, XCB_COPY_FROM_PARENT, window, root, 0, 0, 300, 200, 50, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, mask, values);
29        xcb_map_window(conn, window);
30    }
31
32    // 创建 ATOM
33    xcb_atom_t nameAtom;
34    {
35        const char* name = "CLIPBOARD";
36        xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, 0, strlen(name), name);
37        xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(conn, cookie, NULL);
38        nameAtom = reply->atom;
39        free(reply);
40    }
41
42    // 创建 ATOM
43    xcb_atom_t utf8StrAtom;
44    {
45        const char* name = "UTF8_STRING";
46        xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, 0, strlen(name), name);
47        xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(conn, cookie, NULL);
48        utf8StrAtom = reply->atom;
49        free(reply);
50    }
51    
52    // 创建一个 selection 并将自己设为的 owner
53    xcb_set_selection_owner(conn, window, nameAtom, XCB_TIME_CURRENT_TIME);
54
55    // 检查是否成功
56    xcb_window_t owner;
57    {
58        xcb_get_selection_owner_cookie_t cookie = xcb_get_selection_owner(conn, nameAtom);
59        xcb_get_selection_owner_reply_t* reply = xcb_get_selection_owner_reply(conn, cookie, NULL);
60        owner = reply->owner;
61    }
62
63    if (owner != window)
64    {
65        printf("failed to set selection owner, owner id is 0x%x\n", owner);
66        return 1;
67    }
68
69    // 读取事件
70    xcb_generic_event_t* event;
71    while ((event = xcb_wait_for_event(conn)))
72    {
73        printf("event type: %d\n", event->response_type);
74        // 其它进程请求读取这个 selection
75        if(event->response_type == XCB_SELECTION_REQUEST)
76        {
77            xcb_selection_request_event_t* request = (xcb_selection_request_event_t*)event;
78
79            xcb_selection_notify_event_t response; // 通知对端的事件
80            response.response_type =XCB_SELECTION_NOTIFY;
81            response.requestor = request->requestor;
82            response.selection = request->selection;
83            response.target = request->target;
84            response.property = request->property;
85            response.time = request->time;
86
87            // 只处理 UTF8_STRING
88            if (request->property == XCB_ATOM_NONE || request->target != utf8StrAtom)
89            {
90                response.property = XCB_ATOM_NONE;
91            }
92            else
93            {
94                // 返回当前时间
95                time_t nowTime = time(NULL);
96                char* nowStr = ctime(&nowTime);
97                xcb_change_property(conn, XCB_PROP_MODE_REPLACE, request->requestor, request->property, request->target, 8, strlen(nowStr), nowStr);
98            }
99            
100            xcb_send_event(conn, 1, request->requestor, 0, (char*)&response);
101            xcb_flush(conn);
102
103            printf("done\n");
104        }
105    }
106
107    xcb_disconnect(conn);
108    return 0;
109}

当一个应用进行复制时,实际上是将自己设为了 clipboard 的 owner,并在其它应用请求粘贴时返回数据。 为了避免复制的数据丢失,成为 owner 时要将之前的数据都保存过来。

如果 owner 退出,selection 会丢失。为了避免这一情况,需要一个 clipboard manager 进程作为 clipboard 的 owner。 当其它进程进行复制操作成为 owner 时,clipboard manager应当进行以下操作:

  1. 向新的 owner 询问所有数据,并将这些数据保存起来
  2. 重新将自己声明为 owner